﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Objects.DataClasses;
using System.Reflection;
using SCEM.DTO;
using System.Data.Objects;
using System.Data;
using System.Data.Metadata.Edm;

namespace SCEM.DataLayer
{
    /// <summary>
    /// Implementação abstrata para IDAO (parametrizada em TEntity).
    /// Para casos que a chave primária da entidade seja um Guid chamado Id, basta apenas implementar o método
    /// ConvertToDTO e definir valores apropriados para as propriedades CanEntityBeDisabled, EntityEnabledProperty
    /// e OrderByProperty, além implementar um método ConvertToEntity no DTO correspondente à essa entidade.
    /// </summary>
    /// <typeparam name="TEntity">Uma entidade do EntityFramework.</typeparam>
    public abstract class EntityDAO<TEntity> : IDAO<TEntity> where TEntity : EntityObject, new()
    {
        #region Reflection Utils

        [Obsolete]
        private void UpdateEntity<T>(T databaseEntity, T updatedEntity, string[] propertiesToUpdate)
        {
            Type type = databaseEntity.GetType();
            PropertyInfo[] properties = type.GetProperties();
            foreach (PropertyInfo property in properties)
            {
                if (propertiesToUpdate.Contains(property.Name))
                {
                    Object value = type.GetProperty(property.Name).GetValue(updatedEntity, null);
                    property.SetValue(databaseEntity, value, null);
                }
            }
        }

        /// <summary>
        /// Converte uma entidade do EntityFramework em um DTO para a camada de negócio.
        /// </summary>
        /// <param name="entity">A entidade do EntityFramework a ser convertida.</param>
        /// <param name="context">O contexto do EntityFramework que foi utilizado para recuperar a entidade.</param>
        /// <returns>Um DTO representando a entidade selecionada.</returns>
        protected abstract IDTO<TEntity> ConvertToDTO(TEntity entity, ObjectContext context);

        #endregion

        #region Helpers

        private bool _canEntityBeDisabled = false;        
        /// <summary>
        /// Indica se a entidade pode ser desabilitada.
        /// </summary>
        protected bool CanEntityBeDisabled
        {
            get { return _canEntityBeDisabled; }
            set { _canEntityBeDisabled = value; }
        }

        private string _entityEnabledProperty = "";
        /// <summary>
        /// O nome da propriedade na entidade que indica que ela está habilitada. Aplicável apenas quando a entidade pode ser desabilitada.
        /// </summary>
        protected string EntityEnabledProperty
        {
            get { return _entityEnabledProperty; }
            set { _entityEnabledProperty = value; }
        }

        private string _orderByProperty = "";
        /// <summary>
        /// O nome da propriedade a ser utilizada para ordenação quando é requisitada uma lista de entidades.
        /// </summary>
        protected string OrderByProperty
        {
            get { return _orderByProperty; }
            set { _orderByProperty = value; }
        }

        private string containerName = "SCEMContainer";
        protected string ContainerName
        {
            get
            {
                return containerName;
            }
        }

        private string entitySetName;
        protected string EntitySetName
        {
            get
            {
                using (var context = new SCEMContainer())
                {
                    entitySetName = context.GetEntitySetName(typeof(TEntity).Name);
                }
                return entitySetName;
            }
        }

        #endregion

        #region Métodos IDAO<TEntity>

        public virtual IDTO<TEntity> Save(IDTO<TEntity> dto)
        {
            using (var context = new SCEMContainer())
            {
                TEntity entity = dto.ConvertToEntity(context);
                context.AddObject(EntitySetName, entity);
                int numRows = context.SaveChanges();
                if (numRows < 1)
                    return null;
                else
                    return ConvertToDTO(entity, context);
            }
        }

        public virtual IDTO<TEntity> Update(IDTO<TEntity> dto)
        {
            using (var context = new SCEMContainer())
            {
                TEntity entity = dto.ConvertToEntity(context);
                int numRows = context.SaveChanges();
                if (numRows < 1)
                    return null;
                else
                    return ConvertToDTO(entity, context);
            }
        }

        public virtual IDTO<TEntity> Get(string id)
        {
            Guid entityId;
            try { entityId = new Guid(id); }
            catch (Exception) { return null; }

            using (var context = new SCEMContainer())
            {
                EntityKey key = new EntityKey(ContainerName + "." + EntitySetName, "Id", entityId);
                Object entityObject = null;
                if (context.TryGetObjectByKey(key, out entityObject))
                    return ConvertToDTO((TEntity)entityObject, context);
                else
                    return null;
            }
        }

        public virtual List<IDTO<TEntity>> GetAll(int pageNumber, int resultsPerPage, bool includeDisabledEntities)
        {
            List<IDTO<TEntity>> dtoList = new List<IDTO<TEntity>>();

            int pageNumberIndex = pageNumber - 1;
            if (pageNumberIndex < 0)
                return dtoList;

            if (!CanEntityBeDisabled)
                includeDisabledEntities = true;

            using (var context = new SCEMContainer())
            {
                ObjectSet<TEntity> objectSet = (ObjectSet<TEntity>)context.GetType().GetProperty(EntitySetName).GetValue(context, null);
                IOrderedQueryable<TEntity> query;
                if (includeDisabledEntities)
                    query = objectSet.OrderBy("it." + OrderByProperty);
                else
                    query = objectSet.Where("it." + EntityEnabledProperty + " == true").OrderBy("it." + OrderByProperty);

                // Todas as linhas são carregadas para manter compatibilidade com o SQL Server Compact Edition
                // que não suporta Skip() e Take(). Para o SQL Server Enterprise, seria possivel utilizar:
                // IQueryable<TEntity> query2 = query.Skip(pageNumberIndex * resultsPerPage).Take(resultsPerPage);
                List<TEntity> entityList = query.ToList();

                foreach (TEntity entity in entityList)
                    dtoList.Add(ConvertToDTO(entity, context));

                int skip = pageNumberIndex * resultsPerPage;
                if (dtoList.Count() <= skip)
                    return new List<IDTO<TEntity>>();
                else if (dtoList.Count() < skip + resultsPerPage)
                    return dtoList.GetRange(skip, dtoList.Count() - skip);
                else
                    return dtoList.GetRange(skip, resultsPerPage);
            }
        }

        public virtual int GetTotalEntities(bool includeDisabledEntities)
        {
            if (!CanEntityBeDisabled)
                includeDisabledEntities = true;

            using (var context = new SCEMContainer())
            {
                ObjectSet<TEntity> objectSet = (ObjectSet<TEntity>)context.GetType().GetProperty(EntitySetName).GetValue(context, null);
                IQueryable<TEntity> query;

                if (includeDisabledEntities)
                    query = objectSet;
                else
                    query = objectSet.Where("it." + EntityEnabledProperty + " == true");

                return query.Count();
            }
        }

        public virtual bool Disable(string id)
        {
            if (!_canEntityBeDisabled)
                throw new NotSupportedException();

            Guid entityId;
            try { entityId = new Guid(id); }
            catch (Exception) { return false; }

            using (var context = new SCEMContainer())
            {
                EntityKey key = new EntityKey(ContainerName + "." + EntitySetName, "Id", entityId);
                Object entityObject;
                if (!context.TryGetObjectByKey(key, out entityObject))
                    return false;

                TEntity entity = (TEntity)entityObject;
                entity.GetType().GetProperty(EntityEnabledProperty).SetValue(entity, false, null);
                context.SaveChanges();
                return true;
            }
        }

        #endregion

    }

    /// <summary>
    /// Classe estática com extensões para o ObjectContext do EntityFramework.
    /// </summary>
    public static class ObjectContextExtensions
    {
        /// <summary>
        /// Retorna o nome do EntitySet de determinada entidade.
        /// </summary>
        /// <param name="context">ObjectContext do EntityFramework.</param>
        /// <param name="entityTypeName">A nome da entidade a ser pesquisada.</param>
        /// <returns>O nome do EntitySet associado à entidade.</returns>
        public static string GetEntitySetName(this ObjectContext context, string entityTypeName)
        {
            var container = context.MetadataWorkspace.GetEntityContainer(context.DefaultContainerName, DataSpace.CSpace);
            string entitySetName = (from meta in container.BaseEntitySets
                                    where meta.ElementType.Name == entityTypeName
                                    select meta.Name).FirstOrDefault();
            return entitySetName;
        }
    }
}
